<?php
class ModuleItem
{

    /**
     * @var ModuleItem
     */
    private static $instance;

    /**
     * Get instance of ModuleItem.
     *
     * @return ModuleItem $instance
     */
    public static function get()
    {
        if (!self::$instance) {
            self::$instance = new ModuleItem();
        }
        return self::$instance;
    }

    /**
     * Initialize.
     */
    private function __construct()
    {
    }

    //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
    //-* APIS
    //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
    /**
     * Update item count.
     *
     * @param ModuleItemVo $item
     * @param int $amount possible to be negative number, means minus money
     * @param int $commodityValue default 0
     * @throws SzException 30500
     * @return ModuleItemVo $item
     */
    private function updateItemCount($item, $amount, $commodityValue = 0)
    {
        if ($commodityValue > 0 && $amount > 0) {
            // 增加物品数量，并且价值大于0，默认为增加付费道具
            $isFreeItem = false;
        } else {
            // 增加物品数量，并且价值小于等于0，默认为增加免费道具
            // 扣除物品数量，默认为扣除免费道具，免费道具不足，扣除付费道具，并计算扣除的价值
            $isFreeItem = true;
            $commodityValue = 0;
        }

        $currentCount = $item->getCount() + $amount;

        // validate money enough or not
        if ($amount < 0 && $currentCount < 0) {
            throw new SzException(30102, null, ModuleItemConst::MODULE_NAME); // not enough count
        }

        if ($isFreeItem || $amount < 0) {
            if ($amount < 0 && $item->getFreeCount() < abs($amount)) {
                // means bonus token not enough to pay the amount, also need to minus purchased token
                $changedCount = abs($item->getFreeCount() + $amount);
                $changedCommodityValue = ($item->getPurchasedCount() > 0) ? -$changedCount * ($item->getCommodityValue() / $item->getPurchasedCount()) : 0;
                $item->setFreeCount(0);
                $item->updatePurchasedCount(-$changedCount);
                $item->updateCommodityValue($changedCommodityValue);
            } else {
                $item->updateFreeCount($amount);
            }
        } else {
            // add token
            if ($isFreeItem) {
                $item->updateFreeCount($amount);
            } else {
                $item->updatePurchasedCount($amount);
                $item->updateCommodityValue($commodityValue);
            }
        }

        return $item;
    }

    /**
     * Add item.
     *
     * @param int              $userId
     * @param int              $count
     * @param ModuleItemVoList $itemList
     * @param int | array      $config INT: $itemDefId, ARRAY: $itemSetting
     * @param int              $itemId default NULL, if itemId specified, system will only add into item with specified itemId
     * @param array            $exceptions default array(), 'exceptItemId, ..., exceptItemId'
     * @param boolean          $allowExceedLimit default FALSE, TRUE: if item count exceed the max count of the item own limit, system will not throw exception
     * @param boolean          $allowExceed default FALSE, TRUE: if item count exceed the max count of the user package, system will not throw exception
     * @param int              $commodityValue default 0
     * @param boolean          $isPermanently default false
     * @throws SzException 30102, 30119
     * @return ModuleItemVoList
     */
    public function addItemCount($userId, $count, $itemList, $config, $itemId = null, $exceptions = array(), $allowExceedLimit = false, $allowExceed = false, $commodityValue = 0, $isPermanently = false)
    {
        // check count
        if ($count <= 0) {
            throw new SzException(30102, null, ModuleItemConst::MODULE_NAME);
        }

        $itemSetting = $this->getItemSetting($config);

        // process items
        if ($itemId) {
            $itemList = $this->addItemCountWithItemId($userId, $itemId, $itemSetting, $count, $itemList, $allowExceedLimit, $allowExceed, $commodityValue, $isPermanently);
        } else {
            if ($itemSetting['stackCount'] <= 0) {
                throw new SzException(30119, array(ModuleItemConst::CONFIG_ITEM_DEFS, 'stackCount'), ModuleItemConst::MODULE_NAME);
            } else if ($itemSetting['stackCount'] == 1) {
                // process item that is not stackable
                $itemList = $this->addNotStackableItems($userId, $itemSetting, $count, $itemList, $exceptions, $allowExceedLimit, $allowExceed, $commodityValue, $isPermanently);
            } else {
                // process item that is stackable
                $itemList = $this->addStackableItems($userId, $itemSetting, $count, $itemList, $exceptions, $allowExceedLimit, $allowExceed, $commodityValue, $isPermanently);
            }
        }

        return $itemList;
    }

    /**
     * Reduce item count.
     * <pre>
     * When reduce item count via itemId, and reduce count greater than free count,
     * we need reduce purchased count in this item record
     * </pre
     *
     * @param int              $userId
     * @param int              $itemId
     * @param int              $count, this count shall alwasy be positive number
     * @param ModuleItemVoList $itemList
     * @param int | array      $config INT: $itemDefId, ARRAY: $itemSetting
     * @param boolean          $removeDbRecord default true, whether need to remove db record if item count reduced to 0
     * @throws SzException 30101, 30102, 30105, 30111
     * @return ModuleItemVoList
     */
    public function reduceItemCount($userId, $itemId, $count, $itemList, $config, $removeDbRecord = true)
    {
        $itemSetting = $this->getItemSetting($config);

        // cannot reduce the count of a duration item
        if ($itemSetting['duration'] > 0) {
            throw new SzException(30111, $itemSetting['itemDefId'], ModuleItemConst::MODULE_NAME);
        }
        // check count
        if ($count <= 0) {
            throw new SzException(30102, null, ModuleItemConst::MODULE_NAME);
        }

        // get item
        $item = $itemList->getElement($itemId);
        if (!$item) {
            throw new SzException(30101, null, ModuleItemConst::MODULE_NAME);
        }
        $newItemCount = $item->getCount() - $count;
        if ($newItemCount < 0) {
            throw new SzException(30105, null, ModuleItemConst::MODULE_NAME);
        }

        if ($removeDbRecord && $newItemCount == 0) {
            $itemList->deleteElement($itemId);
        } else {
            // set item count
            $item = $this->updateItemCount($item, -$count);
            $item->setUpdateTime(SzTime::getTime());
            // update item
            $itemList->updateElement($item);
        }

        return $itemList;
    }

    /**
     * Reduce item count according to itemDefId.
     * It will loop all items of the same itemDefId to reduce the item count.
     *
     * @param int              $userId
     * @param int              $itemDefId
     * @param int              $count
     * @param ModuleItemVoList $itemList
     * @param int | array      $config INT: $itemDefId, ARRAY: $itemSetting
     * @param boolean          $removeDbRecord default true, whether need to remove db record if item count reduced to 0
     * @throws SzException 30102, 30105, 30111, 30114
     * @return ModuleItemVoList
     */
    public function reduceItemCountByDefId($userId, $itemDefId, $count, $itemList, $config = null, $removeDbRecord = true)
    {
        $itemSetting = is_array($config) ? $config : $this->getItemSetting($itemDefId);

        // cannot lessen the count of a duration item
        if ($itemSetting['duration'] > 0) {
            throw new SzException(30111, array($itemDefId), ModuleItemConst::MODULE_NAME);
        }
        // check count
        if ($count <= 0) {
            throw new SzException(30102, null, ModuleItemConst::MODULE_NAME);
        }
        // get now time
        $now = SzTime::getTime();

        // retrieve items
        $items = $itemList->getElementsByItemDefId($itemDefId);

        // since maybe some items in insert list with the same itemDefId existing, we have to grab them out here, for reduce actions
        $insertList = $itemList->getInsertList();
        foreach ($insertList as $insertItem) {
            /* @var ModuleItemVo $insertItem */
            if ($insertItem->getItemDefId() == $itemDefId) {
                if (is_null($items)) {
                    $items = array();
                }
                array_unshift($items, $insertItem);
            }
        }

        if (!$items || !is_array($items)) {
            throw new SzException(30114, array($itemDefId), ModuleItemConst::MODULE_NAME);
        }

        // loop to reduce the item count
        $leftCount = $count;
        $leftCount = $this->loopReduceItemCount($items, $itemList, $leftCount, $now, true); // reduce free count
        $leftCount = $this->loopReduceItemCount($items, $itemList, $leftCount, $now, false); // reduce purchased count

        if ($leftCount > 0) {
            // all loop done, but $leftCount is still bigger than 0,
            // means user does not have enough item to reduce
            throw new SzException(30105, null, ModuleItemConst::MODULE_NAME);
        }

        // 统计出所有免费道具数量与付费道具数量，
        /* @var ModuleItemVo $item */
        $freeCount = 0;
        $purchasedCount = 0;
        foreach ($items as $item) {
            if ($item->getCount() > 0) {
                $freeCount += $item->getFreeCount();
                $purchasedCount += $item->getPurchasedCount();
            }
        }

        // 先填满免费道具，再填满付费道具，剩下的道具记录进行删除或将道具数量设为空
        foreach ($items as $item) {
            if ($freeCount > 0 || $purchasedCount > 0) {
                // clear item count
                $item->setFreeCount(0);
                $item->setPurchasedCount(0);

                if ($freeCount > 0) {
                    $diffCount = min($itemSetting['stackCount'], $freeCount); // how many item count current item can store
                    $freeCount -= $diffCount;
                    $item->setFreeCount($diffCount);
                }

                if ($purchasedCount > 0) {
                    $diffCount = min($itemSetting['stackCount'], $purchasedCount); // how many item count current item can store
                    $purchasedCount -= $diffCount;
                    $item->setPurchasedCount($diffCount);
                }

                if (!is_null($item->getItemId())) {
                    $itemList->updateElement($item);
                }
            } else {
                if ($removeDbRecord) {
                    if (is_null($item->getItemId())) {
                        $itemList->removeItemInInsertList($item);
                    } else {
                        $itemList->deleteElement($item->getItemId());
                    }
                } else {
                    $item->setFreeCount(0);
                    $item->setPurchasedCount(0);
                    $item->setUpdateTime($now);
                    if (is_null($item->getItemId())) {
                        $itemList->updateItemCountInInsertList($item, -$leftCount);
                    } else {
                        $itemList->updateElement($item);
                    }
                }
            }
        }

        return $itemList;
    }

    /**
     * Loop reduce item free count or purchased count
     *
     * @param array $items
     * @param ModuleItemVoList $itemList
     * @param int $leftCount
     * @param int $now
     * @param boolean $isFreeItem default true
     * @throws SzException 30500
     * @return int $leftCount
     */
    private function loopReduceItemCount(&$items, $itemList, $leftCount, $now, $isFreeItem = true)
    {
        if ($leftCount <= 0) {
            return $leftCount;
        }

        foreach ($items as $key => $item) {
            /* @var ModuleItemVo $item */
            if ($isFreeItem) {
                $delta = $item->getFreeCount() - $leftCount;
            } else {
                $delta = $item->getPurchasedCount() - $leftCount;
            }

            if ($delta > 0) { // current item count is enough
                if ($isFreeItem) {
                    $item->setFreeCount($delta);
                } else {
                    $item->setPurchasedCount($delta);
                }
                $item->setUpdateTime($now);
                if (is_null($item->getItemId())) {
                    $itemList->updateItemCountInInsertList($item, -$leftCount);
                } else {
                    $itemList->updateElement($item);
                }
                $leftCount = 0;
            } else { // current item count is not enough, or just enough and left count is 0
                if ($isFreeItem) {
                    $item->setFreeCount(0);
                } else {
                    $item->setPurchasedCount(0);
                }
                $item->setUpdateTime($now);
                if (is_null($item->getItemId())) {
                    $itemList->updateItemCountInInsertList($item, -$leftCount);
                } else {
                    $itemList->updateElement($item);
                }
                $leftCount = abs($delta);
            }

            $items[$key] = $item;

            if ($leftCount <= 0) {
                // $leftCount equals to 0 means item count reduce done, break out of the loop
                break;
            }
        }

        return $leftCount;
    }

    /**
     * Buy item.
     *
     * @param int              $userId
     * @param int              $count
     * @param int              $priceId
     * @param int | array      $config INT: $itemDefId, ARRAY: $itemSetting
     * @param ModuleItemVoList $itemList default null
     * @param boolean          $addIntoPackage default true, if false, item will not be inserted into package
     * @param int              $reqTime default null, logic computing time
     * @param int              $userLevel default 1
     * @param array            $exceptions default array(), 'exceptItemId, ..., exceptItemId'
     * @param int              $commodityValue default 0
     * @param boolean          $isPermanently default false
     * @throws SzException 30106, 30107, 30108, 30117
     * @return array '$itemList => ModuleItemVoList, $itemSetting['priceId'] => int, $priceCount => int'
     */
    public function buyItem($userId, $count, $priceId, $config, $itemList = null, $addIntoPackage = true, $reqTime = null, $userLevel = 1, $exceptions = array(), $commodityValue = 0, $isPermanently = false)
    {
        $itemSetting = $this->getItemSetting($config);
        $itemDefId = $itemSetting['itemDefId'];
        $storeSetting = SzConfig::get()->loadAppConfig(ModuleItemConst::CONFIG_ITEMS_STORE, $itemDefId, true);

        // prepare params
        $now = (is_null($reqTime)) ? SzTime::getTime() : $reqTime;
        $onShelf = SzTime::getTimestamp($storeSetting['onShelf']);
        $offShelf = SzTime::getTimestamp($storeSetting['offShelf']);
        if (is_null($itemList)) {
            $itemList = SzPersister::get()->getVoList($userId, ModuleItemConst::ORM_NAME_ITEM);
        }
        $priceSetting = $storeSetting['price']; // array($priceId => $count, ...)

        // check purchase price type
        if (!SzUtility::checkArrayKey($priceId, $priceSetting)) {
            throw new SzException(30117, array($priceId), ModuleItemConst::MODULE_NAME);
        } else {
            $priceCount = $priceSetting[$priceId]; // single piece price
        }

        // validation
        if (($onShelf && $now < $onShelf) || ($offShelf && $now > $offShelf)) { // not on shelf
            throw new SzException(30106, $itemDefId, ModuleItemConst::MODULE_NAME);
        }
        if ($userLevel < $storeSetting['level']) { // level not match
            throw new SzException(30107, $itemDefId, ModuleItemConst::MODULE_NAME);
        }

        // check discount
        $discountStart = SzTime::getTimestamp($storeSetting['discountStart']);
        $discountEnd = SzTime::getTimestamp($storeSetting['discountEnd']);
        $discount = 1;
        if ($now >= $discountStart && $now <= $discountEnd) {
            $discount = $storeSetting['discount'];
        }
        // get price
        $priceCount = intval($priceCount * $discount * $count);

        // add item
        if ($addIntoPackage) {
            $itemList = $this->addItemCount($userId, $count * $storeSetting['count'], $itemList, $itemSetting, null, $exceptions, false, false, $commodityValue, $isPermanently); // exceeding in buying is not allowed
        } else {
            if ($isPermanently) {
                ModuleItem::get()->addModuleItemDurationLog($userId, $itemDefId, ModuleItemConst::PERMANENTLY_ITEM_EXPIRE_TIME, $commodityValue);
            }
        }

        // the priceId here can be any virtual currency defined in the application
        return array($itemList, $priceId, $priceCount);
    }

    /**
     * Recycle item (sell item or destory item).
     *
     * @param int              $userId
     * @param int              $count
     * @param ModuleItemVoList $itemList
     * @param int | array      $config INT: $itemDefId, ARRAY: $itemSetting
     * @param boolean          $sellItemInPackage default true, sometimes item sold is not in the package (false), then do not reduce the count
     * @param int              $itemId default NULL, if itemId specified, system will only recycle the items with specified itemId
     * @param boolean          $removeDbRecord default true, whether need to remove db record if item count reduced to 0
     * @throws SzException 30110
     * @return array '$itemList => ModuleItemVoList, $itemSetting['recycleId'] => int, $recycleCount => int'
     */
    public function recycleItem($userId, $count, $itemList, $config, $sellItemInPackage = true, $itemId = null, $removeDbRecord = true)
    {
        $itemSetting = $this->getItemSetting($config);

        if (!($itemSetting['binding'] & ModuleItemConst::BINDING_RECYCLABLE_LIMIT_1
            || $itemSetting['binding'] & ModuleItemConst::BINDING_RECYCLABLE_LIMIT_2
            || $itemSetting['binding'] & ModuleItemConst::BINDING_RECYCLABLE_LIMIT_3
            || $itemSetting['binding'] & ModuleItemConst::BINDING_RECYCLABLE_LIMIT_4)) {
            throw new SzException(30110, $itemSetting['itemDefId'], ModuleItemConst::MODULE_NAME);
        }

        // reduce item count
        if ($sellItemInPackage) {
            if ($itemId) {
                $itemList = $this->reduceItemCount($userId, $itemId, $count, $itemList, $itemSetting, $removeDbRecord);
            } else {
                $itemList = $this->reduceItemCountByDefId($userId, $itemSetting['itemDefId'], $count, $itemList, $itemSetting, $removeDbRecord);
            }
        }

        // check recycle count
        $recycleCount = 0;
        if ($itemSetting['recycleCount']) { // recycle virtual currency count configured
            $recycleCount = $itemSetting['recycleCount'] * $count;
        }

        return array($itemList, $itemSetting['recycleId'], $recycleCount);
    }

    /**
     * Purchase item package grid volume.
     *
     * @param int $userId
     * @param int $targetGridId target grid number id
     * @param int $priceId
     * @param ModuleItemVoList $itemList default null
     * @throws SzException 30113
     * @return array '$itemList => ModuleItemVoList, $priceId => int, $cost => int'
     */
    public function purchasePackageVolume($userId, $targetGridId, $priceId, $itemList = null)
    {
        // read price config
        /**
         * array(
         *     gridId => array(
         *         'gridId' => int,
         *         'price' => array(
         *             priceId => priceCount,
         *             ...
         *         ),
         *         ...
         *     ),
         *     ...
         * )
         * grid starts from 0
         */
        $configs = SzConfig::get()->loadAppConfig(ModuleItemConst::CONFIG_ITEM_PACKAGE, null, true);

        // validate grid purchase limit
        if ($targetGridId > (count($configs) - 1) || $targetGridId < 0) {
            throw new SzException(30113, null, ModuleItemConst::MODULE_NAME);
        }

        // retrieve objects
        if (is_null($itemList)) {
            /* @var ModuleItemVoList $itemList */
            $itemList = SzPersister::get()->getVoList($userId, ModuleItemConst::ORM_NAME_ITEM);
        }
        /**
         * @var ModuleItemVolumeVo $volume
         */
        $volume = SzPersister::get()->getVo($userId, ModuleItemConst::ORM_NAME_ITEM_VOLUME);

        $startGridId = $volume->getGridId() + 1;
        // get grid configs to be purchased
        $gridConfigsTobePurchased = array();
        if ($startGridId == $targetGridId) { // purchase one grid
            $gridConfigsTobePurchased = array($configs[$targetGridId]);
        } else { // purchase multi grids
            $gridConfigsTobePurchased = array_slice($configs, $startGridId, ($targetGridId - $startGridId + 1));
        }

        // calculate cost
        $cost = 0;
        if ($gridConfigsTobePurchased && is_array($gridConfigsTobePurchased)) {
            foreach ($gridConfigsTobePurchased as $gridConfig) {
                $cost += $gridConfig['price'][$priceId];
            }
        }

        // update
        $volume->setGridId($targetGridId);
        SzPersister::get()->setVo($volume);
        // update purchased package volume in itemList
        $itemList->setPurchasedVolume($targetGridId + 1);
        SzPersister::get()->setVoList($itemList);

        return array($itemList, $priceId, $cost);
    }

    /**
     * Ensure target $priceId is valid to item with $itemDefId.
     * And get it.
     *
     * @param int $itemDefId
     * @param int $priceId
     * @param array|null $itemStoreSetting
     * @throws SzException 30118
     * @return int $priceCount
     */
    public function getItemPriceSetting($itemDefId, $priceId, $itemStoreSetting = null)
    {
        if (is_null($itemStoreSetting)) {
            $itemStoreSetting = SzConfig::get()->loadAppConfig(ModuleItemConst::CONFIG_ITEMS_STORE, $itemDefId, true);
        }

        if (!SzUtility::checkArrayKey('price', $itemStoreSetting)) {
            throw new SzException(30118, array($itemDefId, $priceId));
        } else if (!SzUtility::checkArrayKey($priceId, $itemStoreSetting['price'])) {
            throw new SzException(30118, array($itemDefId, $priceId));
        }

        return $itemStoreSetting['price'][$priceId];
    }

    //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
    //-* SUB LOGIC FUNCTIONS
    //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
    /**
     * Add item into record with specified itemId.
     * @param int              $userId
     * @param int              $itemId
     * @param array            $itemSetting
     * @param int              $count
     * @param ModuleItemVoList $itemList
     * @param boolean          $allowExceedLimit
     * @param boolean          $allowExceed
     * @param int              $commodityValue
     * @param boolean          $isPermanently default false
     * @throws SzException 30104, 30115, 30116, 30119
     * @return ModuleItemVoList $itemList
     */
    private function addItemCountWithItemId($userId, $itemId, $itemSetting, $count, $itemList, $allowExceedLimit, $allowExceed, $commodityValue, $isPermanently = false)
    {
        $itemDefId = $itemSetting['itemDefId'];
        $now = SzTime::getTime();

        $item = $itemList->getElement($itemId);
        if ($itemSetting['stackCount'] <= 0) {
            throw new SzException(30119, array(ModuleItemConst::CONFIG_ITEM_DEFS, 'stackCount'), ModuleItemConst::MODULE_NAME);
        } else if ($itemSetting['stackCount'] == 1
            && $itemSetting['duration']) { // not stackable duration item, invalid
            throw new SzException(30116, null, ModuleItemConst::MODULE_NAME);
        } else if ($itemSetting['duration']) { // stackable duration item, valid
            $itemList = $this->extendItemExpireTime($userId, $item, $count, $itemList, $itemSetting, $now, $allowExceed, $commodityValue);
        } else {
            // validate item count of itemId
            $newItemCount = $item->getCount() + $count;
            if ($newItemCount > $itemSetting['stackCount']) {
                throw new SzException(30115, null, ModuleItemConst::MODULE_NAME);
            }
            // validate total item count of itemDefId
            $totalCount = $itemList->getItemDefCount($itemDefId); // total count of this itemDefId
            if ($allowExceedLimit === false
                && (($totalCount + $count) > $itemSetting['ownLimit'])) {
                throw new SzException(30104, null, ModuleItemConst::MODULE_NAME);
            }
            // add item count
            if ($isPermanently) {
                $item = $this->updateItemCount($item, $count, 0);
                $item->setCommodityValue($commodityValue);
                $item->setExpireTime(ModuleItemConst::PERMANENTLY_ITEM_EXPIRE_TIME);
            } else {
                $item = $this->updateItemCount($item, $count, $commodityValue);
            }

            $item->setUpdateTime($now);
            $itemList->updateElement($item, $allowExceed);
        }

        return $itemList;
    }

    /**
     * Add stackable items into itemList.
     * @param int              $userId
     * @param array            $itemSetting
     * @param int              $count
     * @param ModuleItemVoList $itemList
     * @param array            $exceptions 'exceptItemId, ..., exceptItemId'
     * @param boolean          $allowExceedLimit
     * @param boolean          $allowExceed
     * @param int              $commodityValue
     * @param int              $isPermanently default 0
     * @throws SzException 30104
     * @return ModuleItemVoList $itemList
     */
    private function addStackableItems($userId, $itemSetting, $count, $itemList, $exceptions, $allowExceedLimit, $allowExceed, $commodityValue, $isPermanently = 0)
    {
        $itemDefId = $itemSetting['itemDefId'];
        $itemType = $itemSetting['type'];
        $now = SzTime::getTime();

        $items = $itemList->getElementsByItemDefId($itemDefId);
        // remove exception items from target item array
        if ($exceptions) {
            foreach ($exceptions as $exceptItemId) {
                unset($items[$exceptItemId]);
            }
        }

        // since maybe some items in insert list with the same itemDefId existing, we have to grab them out here, for add actions
        $insertList = $itemList->getInsertList();
        foreach ($insertList as $insertItem) {
            /* @var ModuleItemVo $insertItem */
            if ($insertItem->getItemDefId() == $itemDefId) {
                if (is_null($items)) {
                    $items = array();
                }
                array_unshift($items, $insertItem);
            }
        }

        if ($itemSetting['duration']) {
            // this item is a duration item
            $item = ($items && is_array($items)) ? current($items) : null; // find existing record
            if (!$item) {
                $itemList = $this->extendItemExpireTime($userId, null, $count, $itemList, $itemSetting, $now, $allowExceed, $commodityValue);
            } else {
                $itemList = $this->extendItemExpireTime($userId, $item, $count, $itemList, $itemSetting, $now, $allowExceed, $commodityValue);
            }
        } else {
            // this item is a count item
            $totalCount = $itemList->getItemDefCount($itemDefId); // total item count of this itemDefId, used to validate the ownLimit
            $singleCommodityValue = $commodityValue / $count;

            // update ModuleItemVo
            if ($items && is_array($items)) {
                foreach ($items as $item) {
                    /* @var ModuleItemVo $item */
                    if ($count <= 0) {
                        break;
                    }
                    // initialize item count
                    $itemCount = $item->getCount();
                    // get new item count
                    $diffCount = min($itemSetting['stackCount'] - $itemCount, $count); // how many item count current item can store
                    $count -= $diffCount;
                    // add item count
                    if ($isPermanently) {
                        $item = $this->updateItemCount($item, $diffCount, 0);
                        $item->setCommodityValue(0); // permanent item have no commodity value
                        $item->setExpireTime(ModuleItemConst::PERMANENTLY_ITEM_EXPIRE_TIME);
                    } else {
                        $item = $this->updateItemCount($item, $diffCount, $singleCommodityValue * $diffCount);
                    }

                    $item->setUpdateTime($now);
                    if (is_null($item->getItemId())) {
                        // means this item is in insertList, no need to "updateElement", but "updateItemCountInInsertList"
                        $itemList->updateItemCountInInsertList($item, $diffCount);
                    } else {
                        $itemList->updateElement($item, $allowExceed);
                    }

                    // get new total count
                    $totalCount += $diffCount;
                }
            }
            // still items not added after the loop of all existing items
            if ($count > 0) {
                $totalCount += $count;
                // means current existing item records of this itemDefId are all full filled,
                // have to create new records to fill other items left
                while ($count > 0) {
                    // no record found, go through normal add item logic
                    $addedCount = min($itemSetting['stackCount'], $count);
                    if ($isPermanently) {
                        $freeCount      = $addedCount;
                        $purchasedCount = 0;
                        $expireTime     = ModuleItemConst::PERMANENTLY_ITEM_EXPIRE_TIME;
                    } else {
                        $freeCount      = ($singleCommodityValue > 0) ? 0 : $addedCount;
                        $purchasedCount = ($singleCommodityValue > 0 ) ? $addedCount : 0;
                        $expireTime     = null;
                    }

                    $item = new ModuleItemVo(
                        null, $userId, $itemDefId, $itemType,
                        $freeCount,  // freeCount
                        $purchasedCount,  // purchasedCount
                        $singleCommodityValue * $addedCount, $expireTime, $now, true
                    );
                    $itemList->addElement($item, $allowExceed);
                    $count -= $addedCount;
                }
            }
            // check item own limit
            if ($allowExceedLimit === false
                && $totalCount > $itemSetting['ownLimit']) {
                throw new SzException(30104, null, ModuleItemConst::MODULE_NAME);
            }
        }

        return $itemList;
    }

    /**
     * Add not stackable items into itemList.
     * @param int              $userId
     * @param array            $itemSetting
     * @param int              $count
     * @param ModuleItemVoList $itemList
     * @param array            $exceptions 'exceptItemId, ..., exceptItemId'
     * @param boolean          $allowExceedLimit
     * @param boolean          $allowExceed
     * @param int              $commodityValue
     * @param int              $isPermanently default 0
     * <pre>
     *  array(durationLogCommodityValue, durationLogExpireTime, itemExpireTime)
     * </pre>
     * @throws SzException 30104
     * @return ModuleItemVoList $itemList
     */
    private function addNotStackableItems($userId, $itemSetting, $count, $itemList, $exceptions, $allowExceedLimit, $allowExceed, $commodityValue, $isPermanently = 0)
    {
        // -*-*-*-*-
        // P.S: Note, if an item is not stackable, even it's a duration item, it's expire time cannot be extended,
        // new item record would be created, for every count of adding request
        // -*-*-*-*-
        $itemDefId = $itemSetting['itemDefId'];
        $itemType = $itemSetting['type'];
        $now = SzTime::getTime();

        $expireTime = null;
        // if this item is a duration item, create expire time
        if ($itemSetting['duration']) {
            $expireTime = $now + $itemSetting['duration'];
        }

        // check existing records, fill the existing records if count is 0 or item expired
        $items = $itemList->getElementsByItemDefId($itemDefId);
        // remove exception items from target item array
        if ($exceptions) {
            foreach ($exceptions as $exceptItemId) {
                unset($items[$exceptItemId]);
            }
        }

        $singleCommodityValue = ($count > 0) ? $commodityValue  / $count : 0;
        if ($items && $itemSetting['duration']) {
            // this item is a duration item
            foreach ($items as $existingItem) {
                /* @var ModuleItemVo $existingItem */
                if ($count <= 0) {
                    break;
                } else if ($existingItem->getExpireTime() <= $now
                    && $count > 0) { // target item is duration item & current item is expired
                    $existingItem->setExpireTime($expireTime);
                    $existingItem->setUpdateTime($now);
                    $existingItem->setCommodityValue($singleCommodityValue);
                    $itemList->updateElement($existingItem, $allowExceed);
                    --$count;
                }
            }
        } else if ($items) {
            // this item is a count item
            $totalCount = $itemList->getItemDefCount($itemDefId); // total item count of this itemDefId, used to validate the ownLimit
            foreach ($items as $existingItem) {
                /* @var ModuleItemVo $existingItem */
                if ($count <= 0) {
                    break;
                } else if ($existingItem->getCount() == 0) {
                    // add item count
                    if ($isPermanently) {
                        $existingItem = $this->updateItemCount($existingItem, 1, 0);
                        $existingItem->setCommodityValue($singleCommodityValue);
                        $existingItem->setExpireTime(ModuleItemConst::PERMANENTLY_ITEM_EXPIRE_TIME);
                    } else {
                        $existingItem = $this->updateItemCount($existingItem, 1, $singleCommodityValue);
                    }
                    $existingItem->setUpdateTime($now);
                    $itemList->updateElement($existingItem, $allowExceed);
                    --$count; // left not filled item count reduced
                    ++$totalCount; // total count increased
                }
            }
        }

        // still items not added after the loop of all existing items
        if ($count > 0) {
            // check item own limit
            if ($allowExceedLimit === false
                && isset($totalCount)
                && ($totalCount > $itemSetting['ownLimit'])) {
                throw new SzException(30104, null, ModuleItemConst::MODULE_NAME);
            }
            // means current existing item records of this itemDefId are all full filled,
            // have to create new records to fill other items left
            if ($isPermanently || $itemSetting['duration']) {
                $freeCount = 1;
                $purchasedCount = 0;
                $expireTime = ($isPermanently) ? ModuleItemConst::PERMANENTLY_ITEM_EXPIRE_TIME : $expireTime;
            } else {
                $freeCount = ($singleCommodityValue > 0) ? 0 : 1;
                $purchasedCount = ($singleCommodityValue > 0) ? 1 : 0;
            }

            $item = new ModuleItemVo(
                null, $userId, $itemDefId, $itemType,
                $freeCount, // freeCount
                $purchasedCount, // purchasedCount
                $singleCommodityValue, $expireTime, $now, true);
            for ($i = 0; $i < $count; ++$i) {
                $itemList->addElement($item, $allowExceed);
            }
        }

        return $itemList;
    }

    /**
     * Extend the expire time of an item, and update this item in the list.
     * @param int                 $userId
     * @param ModuleItemVo | NULL $item can be NULL, means create a new item with expire time and add it into itemList
     * @param int                 $count
     * @param ModuleItemVoList    $itemList
     * @param array               $itemSetting
     * @param int                 $now current timestamp
     * @param boolean             $allowExceed
     * @param int                 $commodityValue
     * @throws SzException 30103
     * @return ModuleItemVoList   $itemList
     */
    private function extendItemExpireTime($userId, $item, $count, $itemList, $itemSetting, $now, $allowExceed, $commodityValue)
    {
        $itemDefId = $itemSetting['itemDefId'];
        $itemType = $itemSetting['type'];

        if (!$item) {
            // no existing record, create one with expire time
            $expireTime = $now + $itemSetting['duration'] * $count;
            if ($expireTime > ModuleItemConst::MAX_USE_PERIOD) {
                throw new SzException(30103, null, ModuleItemConst::MODULE_NAME);
            }
            $item = new ModuleItemVo(
                null, $userId, $itemDefId, $itemType,
                1, // freeCount
                0, // purchasedCount
                $commodityValue, $expireTime, $now, true
            );
            $itemList->addElement($item, $allowExceed);
        } else {
            // calculate new item expire time
            $expireTime = max($item->getExpireTime(), $now);
            $expireTime += $itemSetting['duration'] * $count;
            if ($expireTime > ModuleItemConst::MAX_USE_PERIOD) {
                throw new SzException(30103, null, ModuleItemConst::MODULE_NAME);
            }
            // update item
            $item->setCommodityValue($commodityValue);
            $item->setExpireTime($expireTime);
            $item->setUpdateTime($now);
            if (!is_null($item->getItemId())) {
                // means this item does not exist in insertList, it's an item need to be updated in updateList
                $itemList->updateElement($item, $allowExceed);
            }
        }

        return $itemList;
    }

    //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
    //-* UTILITIES
    //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
    /**
     * Get item purchase records from cache.
     * @param int $userId
     * @return array $records = array (
     *                   'timestamp' => latest item purchasing timestamp,
     *                   'records' => array(
     *                       'itemDefId' => 'count',
     *                       ...
     *                   ),
     *               );
     */
    public function getItemPurchaseRecords($userId)
    {
        $records = SzContextFactory::get()->getAppCache()->get('ITEM-PRD-' . $userId);

        if ($records) {
            $records = json_decode($records, true);
        } else {
            $records = array();
        }

        return $records;
    }

    /**
     * Set item purchase records into cache.
     * @param int $userId
     * @param array $records
     * @return boolean $result
     */
    public function setItemPurchaseRecords($userId, $records)
    {
        return SzContextFactory::get()->getAppCache()->set('ITEM-PRD-' . $userId, json_encode($records), SzTime::DAY2);
    }

    /**
     * Get item setting.
     * Since the config parameter could be itemSetting itself, this utility function could help.
     * @param int | array $config INT: $itemDefId, ARRAY: $itemSetting
     * @return array $itemSetting
     */
    private function getItemSetting($config)
    {
        if (!is_array($config)) { // config is: int type itemDefId
            $itemSetting = SzConfig::get()->loadAppConfig(ModuleItemConst::CONFIG_ITEM_DEFS, $config, true);
        } else { // config is: $itemSetting itself
            $itemSetting = $config;
        }

        return $itemSetting;
    }

    /**
     * add Duration Log
     *
     * @param int $userId
     * @param int $itemDefId
     * @param int $expireTime
     * @param int $commodityValue
     * @return void
     */
    public function addModuleItemDurationLog($userId, $itemDefId, $expireTime, $commodityValue)
    {
        $now = SzSystem::getReqTime();
        $expireTime = ($expireTime == ModuleItemConst::PERMANENTLY_ITEM_EXPIRE_TIME) ? $now + SzTime::HOURS24 * 180 : $expireTime;

        /**
         * @var ModuleItemDurationLogVoList $itemDurationList
         * @var ModuleItemDurationLogVo $itemDuration
         */
        $itemDurationList = SzPersister::get()->getVoList($userId, ModuleItemConst::ORM_NAME_ITEM_DURATION_LOG);
        $itemDuration = new ModuleItemDurationLogVo(
            null, // id
            $userId, // uid
            $itemDefId, // itemid
            $now, // c_time
            $expireTime, // expire_time
            $commodityValue, // c_value
            true
        );
        $itemDurationList->addElement($itemDuration);
        SzPersister::get()->setVoList($itemDurationList);
    }
}